查看原文
其他

让人欲罢不能的空指针(NullPointerException)

2017-12-04

作者 keliuyue

本文作者


作者:keliuyue

链接:http://dwz.cn/6WC4Fm

本文由作者投稿发布。


周一,给大家来一篇轻松点的文章,对于一些常见的NPE,可以列在一张纸上,没事看看,或者上线前检查下代码,避免出现线上问题。


定义:NullPointerException是java.lang.NullPointerException的简称,是Java语言中的一个异常类,位于java.lang包中,父类是java.lang.RuntimeException,该异常在源程序中可以不进行捕获和处理。


发生频率: ★★★★★


发生原因:当应用程序试图在需要对象的地方使用时,抛出该异常。这种情况包括:


  • 调用 null 对象的实例方法。

  • 访问或修改 null 对象的字段。

  • 将 null 作为一个数组,获得其长度。

  • 将 null 作为一个数组,访问或修改其时间片。

  • 将 null 作为 Throwable 值抛出。

  • Android 中View没有初始化


应用程序应该抛出该类的实例,指示其他对 null 对象的非法使用。


1简单例子分析


1.1 具体事例


现在看一个我们经常在log中出现的事例,下面我们看一下这个Log


Exception in thread "main" java.lang.NullPointerException
       at com.example.myproject.Book.getTitle(Book.java:16)
       at com.example.myproject.Author.getBookTitles(Author.java:25)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

1.2 分析


我们简单分析一下,这个是我们所遇到的问题。最上面一行表示我们遇到的是java.lang.NullPointerException问题,然后我们需要特别关注at这个单词,它可以告诉我们问题发生的具体位置。然后我们找到相应的位置并解决问题。


我们最上面的at提示的是


at com.example.myproject.Book.getTitle(Book.java:16)


我们通过debug,首先找到Book.java类,然后看一下第16行


15   public String getTitle() {
16      System.out.println(title.toString());
17      return title;
18   }


通过上面的代码,因为我们报错的方法是getTitle,然后这个方法返回的String值,如果为空的化,只有可能title 为空。所以可以了解到基本上就是title 为null


2一连串exceptions的例子


2.1具体事例


有时候APP catch的是一个Exception,然而实际抛出的是另一个Exception,像这样的:


34   public void getBookIds(int id) {
35      try {
36         book.getId(id);    // 这个方法抛出NullPointerException在22行
37      } catch (NullPointerException e) {
38         throw new IllegalStateException("A book has a null property", e)
39      }
40   }


然后这个异常的Log是这样的:

Exception in thread "main" java.lang.IllegalStateException: A book has a null property
       at com.example.myproject.Author.getBookIds(Author.java:38)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
       at com.example.myproject.Book.getId(Book.java:22)
       at com.example.myproject.Author.getBookIds(Author.java:36)
       ... 1 more


2.2分析


这个异常中的Caused by有什么不同吗,有时候Log中有很多Caused by。我们希望在这些错误中找到最主要的问题,一般主要的问题出现在最后的Caused by。在这个异常中的主要问题是:


Caused by: java.lang.NullPointerException <-- 主要导致的Exception
       at com.example.myproject.Book.getId(Book.java:22) <-- 最重要的一行


然后我们根据这个异常Log,我们可以定位到Book.java类中22行的getId方法。确定是这行导致的NullPointerException 。然后判断是否有值为null 。


3具体示例


1. 方法中对象参数为空导致的NullPointerException


public void doSomething(SomeObject obj){
  //一些对obj的操作
}


分析


正常看上去没有什么问题,但是这个会出现隐藏的bug,平时我们在编码过程中很容易出现这种细节的问题。当我们使用下列代码的时候。


doSomething(null);


当你传递的参数对象SomeObject为null的时候,例如:


SomeObject obj = null;
doSomething(obj);
// 等同于
doSomething(null);


最后实现的就是这样的。当方法中使用SomeObject对象时。会发生NullPointerException异常。所以在我们操作的过程中。我们需要对传递过来的SomeObject对象进行非空判断,然后在对象不为空的时候进行你需要的操作,然后在为空的时候进行提示或者其他操作。

所以最后我们的处理操作是:


/**
 * @param obj 参数有可能为空
 *
 */

public void doSomething(SomeObject obj){
   if(obj != null){
      //do something
   } else {
      //do something else
   }
}


2. 创建对象数组时候抛出空指针


public class ResultList {
   public String name;
   public Object value;
   public ResultList() {}
}
public class Test {
   public static void main(String[] args){
       ResultList[] boll = new ResultList[5];
       boll[0].name = "iiii";
   }
}


分析


首先看一下main方法中的这一行代码


ResultList[] boll = new ResultList[5];


对象数组已经初始化了,但是数据中的每一个对象并没有初始化。所以我们调用boll[0].name = "iiii";这行代码会报空指针。 我们应该在调用之前进行初始化对象操作。


ResultList[] boll = new ResultList[5];
// 调用之前进行初始化
boll[0] = new ResultList();
boll[0].name = "iiii";


或者另外一种方式初始化


ResultList[] boll = {new ResultList(),new ResultList(),new ResultList(),new ResultList(),new ResultList()};
// 这种数据的定义并对每一个对象进行初始化
boll[0].name = "iiii";


这种方式就是数据的定义和初始化的两种方式。


3. 在Fragment中调用onCreate方法并找控件造成NullPointerException


Fragment的onCreate()方法


@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   View something = findViewById(R.id.something);
   something.setOnClickListener(new View.OnClickListener() { ... }); // NPE HERE
   if (savedInstanceState == null) {
       getSupportFragmentManager().beginTransaction()
               .add(R.id.container, new PlaceholderFragment()).commit();
   }
}


Fragment的布局


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:paddingBottom="@dimen/activity_vertical_margin"
   android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
   android:paddingTop="@dimen/activity_vertical_margin"
   tools:context="packagename.MainActivity$PlaceholderFragment" >
   <View
       android:layout_width="100dp"
       android:layout_height="100dp"
       android:id="@+id/something" />
</RelativeLayout>


分析


首先我们看一张Fragment生命周期的图片



可以知道onCreate方法在onCreateView方法之前执行。然后返回null。然后如果调用方法就造成了NullPointerException 可以使用以下的方法进行


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
   Bundle savedInstanceState)
{
 View rootView = inflater.inflate(R.layout.fragment_main, container,
     false);
 View something = rootView.findViewById(R.id.something); // not activity findViewById()
 something.setOnClickListener(new View.OnClickListener() { ... });
 return rootView;
}


我们等到布局已经创建之后进行findViewById就不会造成NullPointerException


4.使用RecyclerView造成的NullPointerException


报错日志信息


java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.widget.RecyclerView$LayoutManager.onMeasure(android.support.v7.widget.RecyclerView$Recycler, android.support.v7.widget.RecyclerView$State, int, int)' on a null object reference
       at android.support.v7.widget.RecyclerView.onMeasure(RecyclerView.java:1764)
       at android.view.View.measure(View.java:17430)

分析


根据我们上面使用的方法,找到报错行文件。找到有可能发生NullPointerException的位置



void android.support.v7.widget.RecyclerView$LayoutManager.onMeasure


是没有提供LayoutManager造成的,所以我们需要做以下处理


// 还需要找到相应的recyclerView
final LinearLayoutManager layoutManager = new LinearLayoutManager(context);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);


5.findViewById方法引发的NullPointerException


MainActivity


public class MainActivity extends Activity {
   ViewPager pager;
   MyPagerAdapter adapter;
   LinearLayout layout1, layout2, layout3;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       layout1 = (LinearLayout) findViewById(R.id.first_View);
       layout2 = (LinearLayout) findViewById(R.id.second_View);
       layout3 = (LinearLayout) findViewById(R.id.third_View);
       adapter = new MyPagerAdapter();
       pager = (ViewPager) findViewById(R.id.main_pager);
       pager.setAdapter(adapter);
   }
   private class MyPagerAdapter extends PagerAdapter
   
{
       @Override
       public int getCount() {
           return 3;
       }
       @Override
       public Object instantiateItem(ViewGroup collection, int position) {
           LinearLayout l = null;
           if (position == 0 )
           {
               l = layout1;
           }
           if (position == 1)
           {
               l = layout2;
           }
           if (position == 2)
           {
               l = layout3;
           }
               collection.addView(l, position);
               return l;
       }
       @Override
       public boolean isViewFromObject(View view, Object object) {
           return (view==object);
       }
        @Override
        public void destroyItem(ViewGroup collection, int position, Object view) {
                collection.removeView((View) view);
        }
   }
}


activity_main layout


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:orientation="vertical"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:background="#a4c639">

   <android.support.v4.view.ViewPager
                       android:layout_width="match_parent"
                       android:layout_height="match_parent"
                       android:id="@+id/main_pager"/>

</LinearLayout>


activity_first layout


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:id="@+id/first_View">

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@string/hello_world" />

<Button
   android:id="@+id/button1"
   style="?android:attr/buttonStyleSmall"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="Button" />

</LinearLayout>


activity_second layout


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:id="@+id/second_View">

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@string/hello_world" />

</LinearLayout>


And the activity_third layout


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:id="@+id/third_View">

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@string/hello_world" />

</LinearLayout>


分析


首先findViewById在setContentView方法调用完之后调用,并且在setContentView中找需要的控件,如果没有则返回为null。


Sample


当使用setContentView(R.layout.activity_first);在你的代码中

如果你调用layout1 = (LinearLayout) findViewById(R.id.first_View);会返回一个View(因为你的activity_first存在这个控件,并有这个id)


如果你调用layout2 = (LinearLayout) findViewById(R.id.second_View);会返回null(因为在activity_first中找不到这个控件的id)


所以在我们使用控件的时候,如果是findViewById造成的NullPointerException,你就需要在布局文件中寻找是否有该控件和id。


4总结


NullPointerException发生的原因有很多种,我们也有相应的解决方法。主要的方式有:


1. 首先查看报NullPointerException的日志,然后找到具体的位置(1.找到at的位置,2.定位你个人的包名,如2.1中at com.example.myproject.Author.getBookIds),然后进行非空判断或者其他操作


2. 如果日志中没有找到具体的位置,然后需要找到具体报错的外部类名和方法(如6.1中android.support.v7.widget.RecyclerView$LayoutManager.onMeasure),然后进行非空操作或者其他操作


3. Android Activity和Fragment中出现NullPointerException,如果以上方法没有问题,了解他们的生命周期,然后看看执行顺序有没有问题。


4. 数组,对象,集合等调用方法之前需要初始化,List<String> mList = new ArrayList<>();这种类似的操作必不可少。


github原地址:https://github.com/keliuyue/AndroidException/blob/master/contents/java/NullPointerException.md


参考


  1. APP研发录

  2. https://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it/218510218510

  3. https://stackoverflow.com/questions/1922677/nullpointerexception-when-creating-an-array-of-objects

  4. https://stackoverflow.com/questions/23653778/nullpointerexception-accessing-views-in-oncreate

  5. https://stackoverflow.com/questions/3988788/what-is-a-stack-trace-and-how-can-i-use-it-to-debug-my-application-errors

  6. https://stackoverflow.com/questions/19078461/null-pointer-exception-findviewbyid


推荐阅读


上一篇:开发一款商业级Banner控件

超详细Android面试的准备与经历分享



-欢迎投稿-


赞助商

Google、滴滴 与 Udacity 联合开发的 Android 课程,有来自硅谷的实战项目,并提供一对一代码审阅和技术辅导,现在部分课程能免费体验,感兴趣的朋友可以扫下面的二维码。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存